<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Tests\Functional\Type\Parser;

use CuyZ\Valinor\Tests\Fixture\Enum\BackedIntegerEnum;
use CuyZ\Valinor\Tests\Fixture\Enum\BackedStringEnum;
use CuyZ\Valinor\Tests\Fixture\Enum\PureEnum;
use CuyZ\Valinor\Tests\Fixture\Object\AbstractObject;
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithConstants;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\IntegerType;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\ObjectSpecification;
use CuyZ\Valinor\Type\Parser\Lexer\NativeLexer;
use CuyZ\Valinor\Type\Parser\Lexer\SpecificationsLexer;
use CuyZ\Valinor\Type\Parser\LexingParser;
use CuyZ\Valinor\Type\Parser\TypeParser;
use CuyZ\Valinor\Type\StringType;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\ArrayType;
use CuyZ\Valinor\Type\Types\BooleanValueType;
use CuyZ\Valinor\Type\Types\CallableType;
use CuyZ\Valinor\Type\Types\ClassStringType;
use CuyZ\Valinor\Type\Types\EnumType;
use CuyZ\Valinor\Type\Types\FloatValueType;
use CuyZ\Valinor\Type\Types\IntegerRangeType;
use CuyZ\Valinor\Type\Types\IntegerValueType;
use CuyZ\Valinor\Type\Types\InterfaceType;
use CuyZ\Valinor\Type\Types\IntersectionType;
use CuyZ\Valinor\Type\Types\IterableType;
use CuyZ\Valinor\Type\Types\ListType;
use CuyZ\Valinor\Type\Types\MixedType;
use CuyZ\Valinor\Type\Types\NativeBooleanType;
use CuyZ\Valinor\Type\Types\NativeFloatType;
use CuyZ\Valinor\Type\Types\NativeIntegerType;
use CuyZ\Valinor\Type\Types\NegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonEmptyArrayType;
use CuyZ\Valinor\Type\Types\NonEmptyListType;
use CuyZ\Valinor\Type\Types\NonEmptyStringType;
use CuyZ\Valinor\Type\Types\NonNegativeIntegerType;
use CuyZ\Valinor\Type\Types\NonPositiveIntegerType;
use CuyZ\Valinor\Type\Types\NullType;
use CuyZ\Valinor\Type\Types\NumericStringType;
use CuyZ\Valinor\Type\Types\PositiveIntegerType;
use CuyZ\Valinor\Type\Types\ScalarConcreteType;
use CuyZ\Valinor\Type\Types\ShapedArrayType;
use CuyZ\Valinor\Type\Types\StringValueType;
use CuyZ\Valinor\Type\Types\UndefinedObjectType;
use CuyZ\Valinor\Type\Types\UnionType;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use DateTime;
use DateTimeInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use stdClass;

final class LexingParserTest extends TestCase
{
    private TypeParser $parser;

    protected function setUp(): void
    {
        parent::setUp();

        $this->parser = new LexingParser(
            new NativeLexer(new SpecificationsLexer([new ObjectSpecification(mustCheckTemplates: true)])),
        );
    }

    /**
     * @param class-string<Type> $type
     */
    #[DataProvider('parse_valid_types_returns_valid_result_data_provider')]
    public function test_parse_valid_types_returns_valid_result(string $raw, string $transformed, string $type): void
    {
        $result = $this->parser->parse($raw);

        self::assertSame($transformed, $result->toString());
        self::assertInstanceOf($type, $result);
    }

    public static function parse_valid_types_returns_valid_result_data_provider(): iterable
    {
        yield 'Null type' => [
            'raw' => 'null',
            'transformed' => 'null',
            'type' => NullType::class,
        ];

        yield 'Null type - uppercase' => [
            'raw' => 'NULL',
            'transformed' => 'null',
            'type' => NullType::class,
        ];

        yield 'Null type followed by description' => [
            'raw' => 'null lorem ipsum',
            'transformed' => 'null',
            'type' => NullType::class,
        ];

        yield 'True type' => [
            'raw' => 'true',
            'transformed' => 'true',
            'type' => BooleanValueType::class,
        ];

        yield 'True type - uppercase' => [
            'raw' => 'TRUE',
            'transformed' => 'true',
            'type' => BooleanValueType::class,
        ];

        yield 'False type' => [
            'raw' => 'false',
            'transformed' => 'false',
            'type' => BooleanValueType::class,
        ];

        yield 'False type - uppercase' => [
            'raw' => 'FALSE',
            'transformed' => 'false',
            'type' => BooleanValueType::class,
        ];

        yield 'Mixed type' => [
            'raw' => 'mixed',
            'transformed' => 'mixed',
            'type' => MixedType::class,
        ];

        yield 'Mixed type - uppercase' => [
            'raw' => 'MIXED',
            'transformed' => 'mixed',
            'type' => MixedType::class,
        ];

        yield 'Mixed type followed by description' => [
            'raw' => 'mixed lorem ipsum',
            'transformed' => 'mixed',
            'type' => MixedType::class,
        ];

        yield 'Float type' => [
            'raw' => 'float',
            'transformed' => 'float',
            'type' => NativeFloatType::class,
        ];

        yield 'Float type - uppercase' => [
            'raw' => 'FLOAT',
            'transformed' => 'float',
            'type' => NativeFloatType::class,
        ];

        yield 'Float type followed by description' => [
            'raw' => 'float lorem ipsum',
            'transformed' => 'float',
            'type' => NativeFloatType::class,
        ];

        yield 'Positive float value' => [
            'raw' => '1337.42',
            'transformed' => '1337.42',
            'type' => FloatValueType::class,
        ];

        yield 'Positive float value followed by description' => [
            'raw' => '1337.42 lorem ipsum',
            'transformed' => '1337.42',
            'type' => FloatValueType::class,
        ];

        yield 'Negative float value' => [
            'raw' => '-1337.42',
            'transformed' => '-1337.42',
            'type' => FloatValueType::class,
        ];

        yield 'Negative float value followed by description' => [
            'raw' => '-1337.42 lorem ipsum',
            'transformed' => '-1337.42',
            'type' => FloatValueType::class,
        ];

        yield 'Integer type' => [
            'raw' => 'int',
            'transformed' => 'int',
            'type' => NativeIntegerType::class,
        ];

        yield 'Integer type - uppercase' => [
            'raw' => 'INT',
            'transformed' => 'int',
            'type' => NativeIntegerType::class,
        ];

        yield 'Integer type followed by description' => [
            'raw' => 'int lorem ipsum',
            'transformed' => 'int',
            'type' => NativeIntegerType::class,
        ];

        yield 'Integer type (longer version)' => [
            'raw' => 'integer',
            'transformed' => 'int',
            'type' => NativeIntegerType::class,
        ];

        yield 'Integer type (longer version) - uppercase' => [
            'raw' => 'INTEGER',
            'transformed' => 'int',
            'type' => NativeIntegerType::class,
        ];

        yield 'Positive integer type' => [
            'raw' => 'positive-int',
            'transformed' => 'positive-int',
            'type' => PositiveIntegerType::class,
        ];

        yield 'Positive integer type - uppercase' => [
            'raw' => 'POSITIVE-INT',
            'transformed' => 'positive-int',
            'type' => PositiveIntegerType::class,
        ];

        yield 'Positive integer type followed by description' => [
            'raw' => 'positive-int lorem ipsum',
            'transformed' => 'positive-int',
            'type' => PositiveIntegerType::class,
        ];

        yield 'Negative integer type' => [
            'raw' => 'negative-int',
            'transformed' => 'negative-int',
            'type' => NegativeIntegerType::class,
        ];

        yield 'Negative integer type - uppercase' => [
            'raw' => 'NEGATIVE-INT',
            'transformed' => 'negative-int',
            'type' => NegativeIntegerType::class,
        ];

        yield 'Negative integer type followed by description' => [
            'raw' => 'negative-int lorem ipsum',
            'transformed' => 'negative-int',
            'type' => NegativeIntegerType::class,
        ];

        yield 'Non-negative integer type' => [
            'raw' => 'non-negative-int',
            'transformed' => 'non-negative-int',
            'type' => NonNegativeIntegerType::class,
        ];

        yield 'Non-negative integer type - uppercase' => [
            'raw' => 'NON-NEGATIVE-INT',
            'transformed' => 'non-negative-int',
            'type' => NonNegativeIntegerType::class,
        ];

        yield 'Non-negative integer type followed by description' => [
            'raw' => 'non-negative-int lorem ipsum',
            'transformed' => 'non-negative-int',
            'type' => NonNegativeIntegerType::class,
        ];

        yield 'Non-positive integer type' => [
            'raw' => 'non-positive-int',
            'transformed' => 'non-positive-int',
            'type' => NonPositiveIntegerType::class,
        ];

        yield 'Non-positive integer type - uppercase' => [
            'raw' => 'NON-POSITIVE-INT',
            'transformed' => 'non-positive-int',
            'type' => NonPositiveIntegerType::class,
        ];

        yield 'Non-positive integer type followed by description' => [
            'raw' => 'non-positive-int lorem ipsum',
            'transformed' => 'non-positive-int',
            'type' => NonPositiveIntegerType::class,
        ];

        yield 'Positive integer value' => [
            'raw' => '1337',
            'transformed' => '1337',
            'type' => IntegerValueType::class,
        ];

        yield 'Positive integer value followed by description' => [
            'raw' => '1337 lorem ipsum',
            'transformed' => '1337',
            'type' => IntegerValueType::class,
        ];

        yield 'Negative integer value' => [
            'raw' => '-1337',
            'transformed' => '-1337',
            'type' => IntegerValueType::class,
        ];

        yield 'Negative integer value followed by description' => [
            'raw' => '-1337 lorem ipsum',
            'transformed' => '-1337',
            'type' => IntegerValueType::class,
        ];

        yield 'Integer range' => [
            'raw' => 'int<42, 1337>',
            'transformed' => 'int<42, 1337>',
            'type' => IntegerRangeType::class,
        ];

        yield 'Integer range with negative values' => [
            'raw' => 'int<-1337, -42>',
            'transformed' => 'int<-1337, -42>',
            'type' => IntegerRangeType::class,
        ];

        yield 'Integer range with min and max values' => [
            'raw' => 'int<min, max>',
            'transformed' => 'int<min, max>',
            'type' => IntegerRangeType::class,
        ];

        yield 'Integer range followed by description' => [
            'raw' => 'int<42, 1337> lorem ipsum',
            'transformed' => 'int<42, 1337>',
            'type' => IntegerRangeType::class,
        ];

        yield 'String type' => [
            'raw' => 'string',
            'transformed' => 'string',
            'type' => StringType::class,
        ];

        yield 'String type - uppercase' => [
            'raw' => 'STRING',
            'transformed' => 'string',
            'type' => StringType::class,
        ];

        yield 'String type followed by description' => [
            'raw' => 'string lorem ipsum',
            'transformed' => 'string',
            'type' => StringType::class,
        ];

        yield 'Non empty string type' => [
            'raw' => 'non-empty-string',
            'transformed' => 'non-empty-string',
            'type' => NonEmptyStringType::class,
        ];

        yield 'Non empty string type - uppercase' => [
            'raw' => 'NON-EMPTY-STRING',
            'transformed' => 'non-empty-string',
            'type' => NonEmptyStringType::class,
        ];

        yield 'Non empty string type followed by description' => [
            'raw' => 'non-empty-string lorem ipsum',
            'transformed' => 'non-empty-string',
            'type' => NonEmptyStringType::class,
        ];

        yield 'Numeric string type' => [
            'raw' => 'numeric-string',
            'transformed' => 'numeric-string',
            'type' => NumericStringType::class,
        ];

        yield 'Numeric string type - uppercase' => [
            'raw' => 'NUMERIC-STRING',
            'transformed' => 'numeric-string',
            'type' => NumericStringType::class,
        ];

        yield 'Numeric string type followed by description' => [
            'raw' => 'numeric-string lorem ipsum',
            'transformed' => 'numeric-string',
            'type' => NumericStringType::class,
        ];

        yield 'String value with single quote' => [
            'raw' => "'foo'",
            'transformed' => "'foo'",
            'type' => StringValueType::class,
        ];

        yield 'String value with single quote followed by description' => [
            'raw' => "'foo' lorem ipsum",
            'transformed' => "'foo'",
            'type' => StringValueType::class,
        ];

        yield 'String value with double quote' => [
            'raw' => '"foo"',
            'transformed' => '"foo"',
            'type' => StringValueType::class,
        ];

        yield 'String value with double quote followed by description containing quotes' => [
            'raw' => '"foo" lorem ipsum / single quote \' and double quote "',
            'transformed' => '"foo"',
            'type' => StringValueType::class,
        ];

        yield 'String value containing other token' => [
            'raw' => '"foo&bar"',
            'transformed' => '"foo&bar"',
            'type' => StringValueType::class,
        ];

        yield 'Boolean type' => [
            'raw' => 'bool',
            'transformed' => 'bool',
            'type' => NativeBooleanType::class,
        ];

        yield 'Boolean type - uppercase' => [
            'raw' => 'BOOL',
            'transformed' => 'bool',
            'type' => NativeBooleanType::class,
        ];

        yield 'Boolean type (longer version)' => [
            'raw' => 'boolean',
            'transformed' => 'bool',
            'type' => NativeBooleanType::class,
        ];

        yield 'Boolean type (longer version) - uppercase' => [
            'raw' => 'BOOLEAN',
            'transformed' => 'bool',
            'type' => NativeBooleanType::class,
        ];

        yield 'Boolean type followed by description' => [
            'raw' => 'bool lorem ipsum',
            'transformed' => 'bool',
            'type' => NativeBooleanType::class,
        ];

        yield 'Undefined object type' => [
            'raw' => 'object',
            'transformed' => 'object',
            'type' => UndefinedObjectType::class,
        ];

        yield 'Undefined object type - uppercase' => [
            'raw' => 'OBJECT',
            'transformed' => 'object',
            'type' => UndefinedObjectType::class,
        ];

        yield 'Undefined object type followed by description' => [
            'raw' => 'object lorem ipsum',
            'transformed' => 'object',
            'type' => UndefinedObjectType::class,
        ];

        yield 'Array native type' => [
            'raw' => 'array',
            'transformed' => 'array',
            'type' => ArrayType::class,
        ];

        yield 'Array native type - uppercase' => [
            'raw' => 'ARRAY',
            'transformed' => 'array',
            'type' => ArrayType::class,
        ];

        yield 'Array native type followed by description' => [
            'raw' => 'array lorem ipsum',
            'transformed' => 'array',
            'type' => ArrayType::class,
        ];

        yield 'Simple array type' => [
            'raw' => 'float[]',
            'transformed' => 'float[]',
            'type' => ArrayType::class,
        ];

        yield 'Simple array type followed by description' => [
            'raw' => 'float[] lorem ipsum',
            'transformed' => 'float[]',
            'type' => ArrayType::class,
        ];

        yield 'Array type with string array-key' => [
            'raw' => 'array<string, float>',
            'transformed' => 'array<string, float>',
            'type' => ArrayType::class,
        ];

        yield 'Array type with int array-key' => [
            'raw' => 'array<int, float>',
            'transformed' => 'array<int, float>',
            'type' => ArrayType::class,
        ];

        yield 'Array type with array-key' => [
            'raw' => 'array<array-key, float>',
            'transformed' => 'array<float>',
            'type' => ArrayType::class,
        ];

        yield 'Array without array-key' => [
            'raw' => 'array<float>',
            'transformed' => 'array<float>',
            'type' => ArrayType::class,
        ];

        yield 'Array without array-key followed by description' => [
            'raw' => 'array<float> lorem ipsum',
            'transformed' => 'array<float>',
            'type' => ArrayType::class,
        ];

        yield 'Non empty native array' => [
            'raw' => 'non-empty-array',
            'transformed' => 'non-empty-array',
            'type' => NonEmptyArrayType::class,
        ];

        yield 'Non empty native array - uppercase' => [
            'raw' => 'NON-EMPTY-ARRAY',
            'transformed' => 'non-empty-array',
            'type' => NonEmptyArrayType::class,
        ];

        yield 'Non empty native array followed by description' => [
            'raw' => 'non-empty-array lorem ipsum',
            'transformed' => 'non-empty-array',
            'type' => NonEmptyArrayType::class,
        ];

        yield 'Non empty array type with string array-key' => [
            'raw' => 'non-empty-array<string, float>',
            'transformed' => 'non-empty-array<string, float>',
            'type' => NonEmptyArrayType::class,
        ];

        yield 'Non empty array type with int array-key' => [
            'raw' => 'non-empty-array<int, float>',
            'transformed' => 'non-empty-array<int, float>',
            'type' => NonEmptyArrayType::class,
        ];

        yield 'Non empty array type with array-key' => [
            'raw' => 'non-empty-array<array-key, float>',
            'transformed' => 'non-empty-array<float>',
            'type' => NonEmptyArrayType::class,
        ];

        yield 'Non empty array without array-key' => [
            'raw' => 'non-empty-array<float>',
            'transformed' => 'non-empty-array<float>',
            'type' => NonEmptyArrayType::class,
        ];

        yield 'Non empty array without array-key followed by description' => [
            'raw' => 'non-empty-array<float> lorem ipsum',
            'transformed' => 'non-empty-array<float>',
            'type' => NonEmptyArrayType::class,
        ];

        yield 'List native type' => [
            'raw' => 'list',
            'transformed' => 'list',
            'type' => ListType::class,
        ];

        yield 'List native type - uppercase' => [
            'raw' => 'LIST',
            'transformed' => 'list',
            'type' => ListType::class,
        ];

        yield 'List native type followed by description' => [
            'raw' => 'list lorem ipsum',
            'transformed' => 'list',
            'type' => ListType::class,
        ];

        yield 'List type' => [
            'raw' => 'list<float>',
            'transformed' => 'list<float>',
            'type' => ListType::class,
        ];

        yield 'List type followed by description' => [
            'raw' => 'list<float> lorem ipsum',
            'transformed' => 'list<float>',
            'type' => ListType::class,
        ];

        yield 'Non empty list native type' => [
            'raw' => 'non-empty-list',
            'transformed' => 'non-empty-list',
            'type' => NonEmptyListType::class,
        ];

        yield 'Non empty list native type - uppercase' => [
            'raw' => 'NON-EMPTY-LIST',
            'transformed' => 'non-empty-list',
            'type' => NonEmptyListType::class,
        ];

        yield 'Non empty list native type followed by description' => [
            'raw' => 'non-empty-list lorem ipsum',
            'transformed' => 'non-empty-list',
            'type' => NonEmptyListType::class,
        ];

        yield 'Non empty list' => [
            'raw' => 'non-empty-list<float>',
            'transformed' => 'non-empty-list<float>',
            'type' => NonEmptyListType::class,
        ];

        yield 'Non empty list followed by description' => [
            'raw' => 'non-empty-list<float> lorem ipsum',
            'transformed' => 'non-empty-list<float>',
            'type' => NonEmptyListType::class,
        ];

        yield 'Shaped array' => [
            'raw' => 'array{foo: string}',
            'transformed' => 'array{foo: string}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Empty shaped array' => [
            'raw' => 'array{}',
            'transformed' => 'array{}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with single quote key' => [
            'raw' => "array{'foo': string}",
            'transformed' => "array{'foo': string}",
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with double quote key' => [
            'raw' => 'array{"foo": string}',
            'transformed' => 'array{"foo": string}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with several keys' => [
            'raw' => 'array{foo: string, bar: int}',
            'transformed' => 'array{foo: string, bar: int}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with several quote keys' => [
            'raw' => 'array{\'foo\': string, "bar": int}',
            'transformed' => 'array{\'foo\': string, "bar": int}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with no key' => [
            'raw' => 'array{string, int}',
            'transformed' => 'array{0: string, 1: int}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with optional key' => [
            'raw' => 'array{foo: string, bar?: int}',
            'transformed' => 'array{foo: string, bar?: int}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with trailing comma' => [
            'raw' => 'array{foo: string, bar: int,}',
            'transformed' => 'array{foo: string, bar: int}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with reserved keyword as key' => [
            'raw' => 'array{string: string}',
            'transformed' => 'array{string: string}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array followed by description' => [
            'raw' => 'array{foo: string} lorem ipsum',
            'transformed' => 'array{foo: string}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Shaped array with key equal to class name' => [
            'raw' => 'array{stdclass: string}',
            'transformed' => 'array{stdclass: string}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Unsealed shaped array' => [
            'raw' => 'array{foo: string, ...}',
            'transformed' => 'array{foo: string, ...}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Unsealed shaped array with trailing comma' => [
            'raw' => 'array{foo: string, ...,}',
            'transformed' => 'array{foo: string, ...}',
            'type' => ShapedArrayType::class,
        ];

        yield 'Iterable type' => [
            'raw' => 'iterable',
            'transformed' => 'iterable',
            'type' => IterableType::class,
        ];

        yield 'Iterable type - uppercase' => [
            'raw' => 'ITERABLE',
            'transformed' => 'iterable',
            'type' => IterableType::class,
        ];

        yield 'Iterable type with string array-key' => [
            'raw' => 'iterable<string, float>',
            'transformed' => 'iterable<string, float>',
            'type' => IterableType::class,
        ];

        yield 'Iterable type with int array-key' => [
            'raw' => 'iterable<int, float>',
            'transformed' => 'iterable<int, float>',
            'type' => IterableType::class,
        ];

        yield 'Iterable type with array-key' => [
            'raw' => 'iterable<array-key, float>',
            'transformed' => 'iterable<float>',
            'type' => IterableType::class,
        ];

        yield 'Iterable without array-key' => [
            'raw' => 'iterable<float>',
            'transformed' => 'iterable<float>',
            'type' => IterableType::class,
        ];

        yield 'Iterable without array-key followed by description' => [
            'raw' => 'iterable<float> lorem ipsum',
            'transformed' => 'iterable<float>',
            'type' => IterableType::class,
        ];

        yield 'Class string' => [
            'raw' => 'class-string',
            'transformed' => 'class-string',
            'type' => ClassStringType::class,
        ];

        yield 'Class string followed by description' => [
            'raw' => 'class-string lorem ipsum',
            'transformed' => 'class-string',
            'type' => ClassStringType::class,
        ];

        yield 'Class string of class' => [
            'raw' => 'class-string<stdClass>',
            'transformed' => 'class-string<stdClass>',
            'type' => ClassStringType::class,
        ];

        yield 'Class string of class followed by description' => [
            'raw' => 'class-string<stdClass> lorem ipsum',
            'transformed' => 'class-string<stdClass>',
            'type' => ClassStringType::class,
        ];

        yield 'Class string of interface' => [
            'raw' => 'class-string<DateTimeInterface>',
            'transformed' => 'class-string<DateTimeInterface>',
            'type' => ClassStringType::class,
        ];

        yield 'Class string of union' => [
            'raw' => 'class-string<DateTimeInterface|stdClass>',
            'transformed' => 'class-string<DateTimeInterface|stdClass>',
            'type' => ClassStringType::class,
        ];

        yield 'Class string of union containing generic type' => [
            'raw' => 'class-string<stdClass|T>',
            'transformed' => 'class-string<stdClass|T>',
            'type' => ClassStringType::class,
        ];

        yield 'Class name' => [
            'raw' => stdClass::class,
            'transformed' => stdClass::class,
            'type' => ClassType::class,
        ];

        yield 'Class name followed by description' => [
            'raw' => 'stdClass lorem ipsum',
            'transformed' => stdClass::class,
            'type' => ClassType::class,
        ];

        yield 'Abstract class name' => [
            'raw' => AbstractObject::class,
            'transformed' => AbstractObject::class,
            'type' => ClassType::class,
        ];

        yield 'Interface name with no template' => [
            'raw' => DateTimeInterface::class,
            'transformed' => DateTimeInterface::class,
            'type' => InterfaceType::class,
        ];

        yield 'Interface name with one template' => [
            'raw' => SomeInterfaceWithOneTemplate::class . '<string>',
            'transformed' => SomeInterfaceWithOneTemplate::class . '<string>',
            'type' => InterfaceType::class,
        ];

        yield 'Class name with generic with one template' => [
            'raw' => SomeClassWithOneTemplate::class . '<int>',
            'transformed' => SomeClassWithOneTemplate::class . '<int>',
            'type' => ClassType::class,
        ];

        yield 'Class name with generic with three templates' => [
            'raw' => SomeClassWithThreeTemplates::class . '<int, string, float>',
            'transformed' => SomeClassWithThreeTemplates::class . '<int, string, float>',
            'type' => ClassType::class,
        ];

        yield 'Class name with generic with first template without type and second template with type' => [
            'raw' => SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType::class . '<int, stdClass>',
            'transformed' => SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType::class . '<int, stdClass>',
            'type' => ClassType::class,
        ];

        yield 'Class name with generic with template of array-key with string' => [
            'raw' => SomeClassWithTemplateOfArrayKey::class . '<string>',
            'transformed' => SomeClassWithTemplateOfArrayKey::class . '<string>',
            'type' => ClassType::class,
        ];

        yield 'Class name with generic with template of array-key with integer' => [
            'raw' => SomeClassWithTemplateOfArrayKey::class . '<int>',
            'transformed' => SomeClassWithTemplateOfArrayKey::class . '<int>',
            'type' => ClassType::class,
        ];

        yield 'Simple array of class name with no template' => [
            'raw' => stdClass::class . '[]',
            'transformed' => stdClass::class . '[]',
            'type' => CompositeTraversableType::class,
        ];

        yield 'Interface name' => [
            'raw' => DateTimeInterface::class,
            'transformed' => DateTimeInterface::class,
            'type' => InterfaceType::class,
        ];

        yield 'Nullable type' => [
            'raw' => '?string',
            'transformed' => 'null|string',
            'type' => UnionType::class,
        ];

        yield 'Nullable type followed by description' => [
            'raw' => '?string lorem ipsum',
            'transformed' => 'null|string',
            'type' => UnionType::class,
        ];

        yield 'Union type' => [
            'raw' => 'int|float',
            'transformed' => 'int|float',
            'type' => UnionType::class,
        ];

        yield 'Union type with native array' => [
            'raw' => 'array|int',
            'transformed' => 'array|int',
            'type' => UnionType::class,
        ];

        yield 'Union type with simple iterable' => [
            'raw' => 'iterable|int',
            'transformed' => 'iterable|int',
            'type' => UnionType::class,
        ];

        yield 'Union type with simple array' => [
            'raw' => 'int[]|float',
            'transformed' => 'int[]|float',
            'type' => UnionType::class,
        ];

        yield 'Union type with array' => [
            'raw' => 'array<int>|float',
            'transformed' => 'array<int>|float',
            'type' => UnionType::class,
        ];

        yield 'Union type with empty string and other string' => [
            'raw' => "''|'foo'",
            'transformed' => "''|'foo'",
            'type' => UnionType::class,
        ];

        yield 'Union type with enum' => [
            'raw' => PureEnum::class . '|' . BackedStringEnum::class,
            'transformed' => PureEnum::class . '|' . BackedStringEnum::class,
            'type' => UnionType::class,
        ];

        yield 'Union type with class-string' => [
            'raw' => 'class-string|int',
            'transformed' => 'class-string|int',
            'type' => UnionType::class,
        ];

        yield 'Union type with shaped array' => [
            'raw' => 'array{foo: string, bar: int}|string',
            'transformed' => 'array{foo: string, bar: int}|string',
            'type' => UnionType::class,
        ];

        yield 'Union type with shaped array with trailing comma' => [
            'raw' => 'array{foo: string, bar: int,}|string',
            'transformed' => 'array{foo: string, bar: int}|string',
            'type' => UnionType::class,
        ];

        yield 'Union type followed by description' => [
            'raw' => 'int|float lorem ipsum',
            'transformed' => 'int|float',
            'type' => UnionType::class,
        ];

        yield 'Intersection type' => [
            'raw' => 'stdClass&DateTimeInterface',
            'transformed' => 'stdClass&DateTimeInterface',
            'type' => IntersectionType::class,
        ];

        yield 'Intersection type followed by description' => [
            'raw' => 'stdClass&DateTimeInterface lorem ipsum',
            'transformed' => 'stdClass&DateTimeInterface',
            'type' => IntersectionType::class,
        ];

        yield 'Class constant with string value' => [
            'raw' => ObjectWithConstants::class . '::CONST_WITH_STRING_VALUE_A',
            'transformed' => "'some string value'",
            'type' => StringValueType::class,
        ];

        yield 'Class constant with integer value' => [
            'raw' => ObjectWithConstants::class . '::CONST_WITH_INTEGER_VALUE_A',
            'transformed' => '1653398288',
            'type' => IntegerValueType::class,
        ];

        yield 'Class constant with float value' => [
            'raw' => ObjectWithConstants::class . '::CONST_WITH_FLOAT_VALUE_A',
            'transformed' => '1337.42',
            'type' => FloatValueType::class,
        ];

        yield 'Class constant with enum value' => [
            'raw' => ObjectWithConstants::class . '::CONST_WITH_ENUM_VALUE_A',
            'transformed' => BackedIntegerEnum::class . '::FOO',
            'type' => EnumType::class,
        ];

        yield 'Class constant with array value' => [
            'raw' => ObjectWithConstants::class . '::CONST_WITH_ARRAY_VALUE_A',
            'transformed' => "array{string: 'some string value', integer: 1653398288, float: 1337.42}",
            'type' => ShapedArrayType::class,
        ];

        yield 'Class constant with nested array value' => [
            'raw' => ObjectWithConstants::class . '::CONST_WITH_NESTED_ARRAY_VALUE_A',
            'transformed' => "array{nested_array: array{string: 'some string value', integer: 1653398288, float: 1337.42}}",
            'type' => ShapedArrayType::class,
        ];

        yield 'Pure enum' => [
            'raw' => PureEnum::class,
            'transformed' => PureEnum::class,
            'type' => EnumType::class,
        ];

        yield 'Backed integer enum' => [
            'raw' => BackedIntegerEnum::class,
            'transformed' => BackedIntegerEnum::class,
            'type' => EnumType::class,
        ];

        yield 'Backed string enum' => [
            'raw' => BackedStringEnum::class,
            'transformed' => BackedStringEnum::class,
            'type' => EnumType::class,
        ];

        yield 'Pure enum value' => [
            'raw' => PureEnum::class . '::FOO',
            'transformed' => PureEnum::class . '::FOO',
            'type' => EnumType::class,
        ];

        yield 'Backed integer enum value' => [
            'raw' => BackedIntegerEnum::class . '::FOO',
            'transformed' => BackedIntegerEnum::class . '::FOO',
            'type' => EnumType::class,
        ];

        yield 'Backed string enum value' => [
            'raw' => BackedStringEnum::class . '::FOO',
            'transformed' => BackedStringEnum::class . '::FOO',
            'type' => EnumType::class,
        ];

        yield 'Pure enum value with pattern with wildcard at the beginning' => [
            'raw' => PureEnum::class . '::*OO',
            'transformed' => PureEnum::class . '::*OO',
            'type' => EnumType::class,
        ];

        yield 'Pure enum value with pattern with wildcard at the end' => [
            'raw' => PureEnum::class . '::FO*',
            'transformed' => PureEnum::class . '::FO*',
            'type' => EnumType::class,
        ];

        yield 'Pure enum value with pattern with wildcard at the beginning and end' => [
            'raw' => PureEnum::class . '::*A*',
            'transformed' => PureEnum::class . '::*A*',
            'type' => EnumType::class,
        ];

        yield 'value-of<BackedStringEnum>' => [
            'raw' => "value-of<" . BackedStringEnum::class . ">",
            'transformed' => "'foo'|'bar'|'baz'",
            'type' => UnionType::class,
        ];

        yield 'value-of<BackedIntegerEnum>' => [
            'raw' => "value-of<" . BackedIntegerEnum::class . ">",
            'transformed' => "42|404|1337",
            'type' => UnionType::class,
        ];

        yield 'Scalar' => [
            'raw' => 'scalar',
            'transformed' => 'scalar',
            'type' => ScalarConcreteType::class,
        ];

        yield 'Default callable' => [
            'raw' => 'callable',
            'transformed' => 'callable',
            'type' => CallableType::class,
        ];

        yield 'Callable with no parameters' => [
            'raw' => 'callable(): string',
            'transformed' => 'callable(): string',
            'type' => CallableType::class,
        ];

        yield 'Callable with one parameter' => [
            'raw' => 'callable(string): string',
            'transformed' => 'callable(string): string',
            'type' => CallableType::class,
        ];

        yield 'Callable with two parameters without trailing comma' => [
            'raw' => 'callable(string, int): string',
            'transformed' => 'callable(string, int): string',
            'type' => CallableType::class,
        ];

        yield 'Callable with two parameters with trailing comma' => [
            'raw' => 'callable(string, int,): string',
            'transformed' => 'callable(string, int): string',
            'type' => CallableType::class,
        ];

        yield 'Callable or string' => [
            'raw' => 'callable|string',
            'transformed' => 'callable|string',
            'type' => UnionType::class,
        ];
    }

    public function test_multiple_union_types_are_parsed(): void
    {
        $raw = 'int|float|string';

        $unionType = $this->parser->parse($raw);

        self::assertInstanceOf(UnionType::class, $unionType);

        $types = $unionType->types();

        self::assertInstanceOf(IntegerType::class, $types[0]);
        self::assertInstanceOf(NativeFloatType::class, $types[1]);
        self::assertInstanceOf(StringType::class, $types[2]);
    }

    public function test_unexpected_non_traversing_token_throws_exception(): void
    {
        $type = $this->parser->parse('array<>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Unexpected token `>`, expected a valid type.', $type->message());
    }

    public function test_missing_right_union_type_throws_exception(): void
    {
        $type = $this->parser->parse('string|');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Right type is missing for union `string|?`.', $type->message());
    }

    public function test_multiple_intersection_types_are_parsed(): void
    {
        $raw = 'stdClass&DateTimeInterface&DateTime';

        $intersectionType = $this->parser->parse($raw);

        self::assertInstanceOf(IntersectionType::class, $intersectionType);

        $types = $intersectionType->types();

        self::assertInstanceOf(ClassType::class, $types[0]);
        self::assertSame(stdClass::class, $types[0]->className());

        self::assertInstanceOf(InterfaceType::class, $types[1]);
        self::assertSame(DateTimeInterface::class, $types[1]->className());

        self::assertInstanceOf(ClassType::class, $types[2]);
        self::assertSame(DateTime::class, $types[2]->className());
    }

    public function test_missing_right_intersection_type_throws_exception(): void
    {
        $type = $this->parser->parse('DateTimeInterface&');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Right type is missing for intersection `DateTimeInterface&?`.', $type->message());
    }

    public function test_missing_simple_array_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('string[');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The closing bracket is missing for the array expression `string[]`.', $type->message());
    }

    public function test_invalid_array_key_throws_exception(): void
    {
        $type = $this->parser->parse('array<float, string>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid type `array<float, string>`: invalid array-key element(s) `float`, each element must be an integer or a string.', $type->message());
    }

    public function test_invalid_non_empty_array_key_throws_exception(): void
    {
        $type = $this->parser->parse('non-empty-array<float, string>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid type `non-empty-array<float, string>`: invalid array-key element(s) `float`, each element must be an integer or a string.', $type->message());
    }

    public function test_missing_array_comma_throws_exception(): void
    {
        $type = $this->parser->parse('array<int string>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('A comma is missing for `array<int, ?>`.', $type->message());
    }

    public function test_missing_non_empty_array_comma_throws_exception(): void
    {
        $type = $this->parser->parse('non-empty-array<int string>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('A comma is missing for `non-empty-array<int, ?>`.', $type->message());
    }

    public function test_missing_array_subtype_throws_exception(): void
    {
        $type = $this->parser->parse('array<');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The subtype is missing for `array<`.', $type->message());
    }

    public function test_missing_array_subtype_after_key_type_throws_exception(): void
    {
        $type = $this->parser->parse('array<string,');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The subtype is missing for `array<string,`.', $type->message());
    }

    public function test_missing_array_comma_or_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('array<string');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Expected comma or closing bracket after `array<string`.', $type->message());
    }

    public function test_missing_array_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('array<int, string');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The closing bracket is missing for `array<int, string>`.', $type->message());
    }

    public function test_missing_non_empty_array_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('non-empty-array<int, string');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The closing bracket is missing for `non-empty-array<int, string>`.', $type->message());
    }

    public function test_missing_list_subtype_throws_exception(): void
    {
        $type = $this->parser->parse('non-empty-list<');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The subtype is missing for `non-empty-list<`.', $type->message());
    }

    public function test_missing_list_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('list<string');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The closing bracket is missing for `list<string>`.', $type->message());
    }

    public function test_missing_non_empty_list_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('non-empty-list<string');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The closing bracket is missing for `non-empty-list<string>`.', $type->message());
    }

    public function test_invalid_iterable_key_throws_exception(): void
    {
        $type = $this->parser->parse('iterable<float, string>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid type `iterable<float, string>`: invalid array-key element(s) `float`, each element must be an integer or a string.', $type->message());
    }

    public function test_missing_iterable_comma_throws_exception(): void
    {
        $type = $this->parser->parse('iterable<int string>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('A comma is missing for `iterable<int, ?>`.', $type->message());
    }

    public function test_missing_iterable_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('iterable<int, string');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The closing bracket is missing for `iterable<int, string>`.', $type->message());
    }

    public function test_missing_class_string_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('class-string<DateTimeInterface');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The closing bracket is missing for the class string expression `class-string<DateTimeInterface>`.', $type->message());
    }

    public function test_class_string_missing_subtype_throws_exception(): void
    {
        $type = $this->parser->parse('class-string<');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The subtype is missing for `class-string<`.', $type->message());
    }

    public function test_invalid_class_string_type_throws_exception(): void
    {
        $type = $this->parser->parse('class-string<stdClass|int>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid class string `class-string<stdClass|int>`, each element must be a class name or an interface name but found `int`.', $type->message());
    }

    public function test_invalid_left_intersection_member_throws_exception(): void
    {
        $type = $this->parser->parse('int&DateTimeInterface');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid types in intersection `int&DateTimeInterface`, each element must be a class name or an interface name but found `int`.', $type->message());
    }

    public function test_invalid_right_intersection_member_throws_exception(): void
    {
        $type = $this->parser->parse('DateTimeInterface&int');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid types in intersection `DateTimeInterface&int`, each element must be a class name or an interface name but found `int`.', $type->message());
    }

    public function test_shaped_array_closing_bracket_missing_throws_exception(): void
    {
        $type = $this->parser->parse('array{string');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing closing curly bracket in shaped array signature `array{0: string`.', $type->message());
    }

    public function test_shaped_array_closing_bracket_missing_after_other_element_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, foo: string');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing closing curly bracket in shaped array signature `array{0: int, foo: string`.', $type->message());
    }

    public function test_shaped_array_closing_bracket_missing_after_comma_throws_exception(): void
    {
        $type = $this->parser->parse('array{int,');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing closing curly bracket in shaped array signature `array{0: int`.', $type->message());
    }

    public function test_shaped_array_colon_missing_throws_exception(): void
    {
        $type = $this->parser->parse('array{string?');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('A colon symbol is missing in shaped array signature `array{string?`.', $type->message());
    }

    public function test_shaped_array_colon_missing_after_other_element_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, foo?');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('A colon symbol is missing in shaped array signature `array{0: int, foo?`.', $type->message());
    }

    public function test_shaped_array_closing_bracket_missing_after_unfinished_element_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, foo?:');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing element type in shaped array signature `array{0: int, foo?:`.', $type->message());
    }

    public function test_shaped_array_colon_expected_but_other_symbol_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, foo?;');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('A colon symbol is missing in shaped array signature `array{0: int, foo?`.', $type->message());
    }

    public function test_shaped_array_comma_expected_but_other_symbol_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, string]');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Comma missing in shaped array signature `array{0: int, 1: string`.', $type->message());
    }

    public function test_unsealed_shaped_array_with_missing_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, ...');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing closing curly bracket in shaped array signature `array{0: int, ...`.', $type->message());
    }

    public function test_shaped_array_with_unsealed_type_with_missing_closing_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, ...array');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing closing curly bracket in shaped array signature `array{0: int, ...array`.', $type->message());
    }

    public function test_shaped_array_with_invalid_unsealed_type_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, ...string}');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid unsealed type in shaped array `array{0: int, ...string}`, it should be a valid array but `string` was given.', $type->message());
    }

    public function test_shaped_array_with_unsealed_type_followed_by_unexpected_token_throws_exception(): void
    {
        $type = $this->parser->parse('array{int, ...array<string>int|string}');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Unexpected `int|string` after sealed type in shaped array signature `array{0: int, ...array<string>int|string`, expected a `}`.', $type->message());
    }

    public function test_unsealed_shaped_array_without_elements_throws_exception(): void
    {
        $type = $this->parser->parse('array{...array<string>}');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing elements in shaped array signature `array{...array<string>}`.', $type->message());
    }

    public function test_missing_min_value_for_integer_range_throws_exception(): void
    {
        $type = $this->parser->parse('int<');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing min value for integer range, its signature must match `int<min, max>`.', $type->message());
    }

    public function test_invalid_min_value_for_integer_range_throws_exception(): void
    {
        $type = $this->parser->parse('int<string, 1337>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid type `string` for min value of integer range, it must be either `min` or an integer value.', $type->message());
    }

    public function test_missing_comma_for_integer_range_throws_exception(): void
    {
        $type = $this->parser->parse('int<42 1337>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing comma in integer range signature `int<42, ?>`.', $type->message());
    }

    public function test_missing_max_value_for_integer_range_throws_exception(): void
    {
        $type = $this->parser->parse('int<42,');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing max value for integer range, its signature must match `int<42, max>`.', $type->message());
    }

    public function test_invalid_max_value_for_integer_range_throws_exception(): void
    {
        $type = $this->parser->parse('int<42, string>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid type `string` for max value of integer range `int<42, ?>`, it must be either `max` or an integer value.', $type->message());
    }

    public function test_missing_closing_bracket_for_integer_range_throws_exception(): void
    {
        $type = $this->parser->parse('int<42, 1337');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing closing bracket in integer range signature `int<42, 1337>`.', $type->message());
    }

    public function test_missing_closing_single_quote_throws_exception(): void
    {
        $type = $this->parser->parse("'foo");

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame("Closing quote is missing for `'foo`.", $type->message());
    }

    public function test_missing_closing_double_quote_throws_exception(): void
    {
        $type = $this->parser->parse('"foo');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Closing quote is missing for `"foo`.', $type->message());
    }

    public function test_missing_enum_case_throws_exception(): void
    {
        $type = $this->parser->parse(PureEnum::class . '::');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing case name for enum `' . PureEnum::class . '::?`.', $type->message());
    }

    public function test_no_enum_case_found_throws_exception(): void
    {
        $type = $this->parser->parse(PureEnum::class . '::ABC');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Unknown enum case `' . PureEnum::class . '::ABC`.', $type->message());
    }

    public function test_no_enum_case_found_with_wildcard_throws_exception(): void
    {
        $type = $this->parser->parse(PureEnum::class . '::ABC*');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Cannot find enum case with pattern `' . PureEnum::class . '::ABC*`.', $type->message());
    }

    public function test_no_enum_case_found_with_several_wildcards_in_a_row_throws_exception(): void
    {
        $type = $this->parser->parse(PureEnum::class . '::F**O');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Cannot find enum case with pattern `' . PureEnum::class . '::F**O`.', $type->message());
    }

    public function test_enum_with_no_case_throws_exception(): void
    {
        $type = $this->parser->parse(EnumWithNoCase::class);

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Enum `' . EnumWithNoCase::class . '` must have at least one case.', $type->message());
    }

    public function test_missing_specific_enum_case_throws_exception(): void
    {
        $type = $this->parser->parse(PureEnum::class . '::*');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing specific case for enum `' . PureEnum::class . '::?` (cannot be `*`).', $type->message());
    }

    public function test_missing_class_constant_case_throws_exception(): void
    {
        $type = $this->parser->parse(ObjectWithConstants::class . '::');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing case name for class constant `' . ObjectWithConstants::class . '::?`.', $type->message());
    }

    public function test_no_class_constant_case_found_throws_exception(): void
    {
        $type = $this->parser->parse(ObjectWithConstants::class . '::ABC');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Unknown class constant case `' . ObjectWithConstants::class . '::ABC`.', $type->message());
    }

    public function test_no_class_constant_case_found_with_wildcard_throws_exception(): void
    {
        $type = $this->parser->parse(ObjectWithConstants::class . '::ABC*');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Cannot find class constant case with pattern `' . ObjectWithConstants::class . '::ABC*`.', $type->message());
    }

    public function test_no_class_constant_case_found_with_several_wildcards_in_a_row_throws_exception(): void
    {
        $type = $this->parser->parse(ObjectWithConstants::class . '::F**O');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Cannot find class constant case with pattern `' . ObjectWithConstants::class . '::F**O`.', $type->message());
    }

    public function test_missing_generic_closing_bracket_throws_exception(): void
    {
        $genericClassName = stdClass::class;

        $type = $this->parser->parse("$genericClassName<string");

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame("The closing bracket is missing for the generic `$genericClassName<string>`.", $type->message());
    }

    public function test_missing_comma_in_generics_throws_exception(): void
    {
        $className = SomeClassWithThreeTemplates::class;

        $type = $this->parser->parse("$className<int, string bool>");

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame("A comma is missing for the generic `$className<int, string, ?>`.", $type->message());
    }

    public function test_missing_generic_throws_exception(): void
    {
        $className = SomeClassWithThreeTemplates::class;

        $type = $this->parser->parse("$className<int>");

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame("There are 2 missing generics for `$className<int, ?, ?>`.", $type->message());
    }

    public function test_superfluous_generic_throws_exception(): void
    {
        $className = SomeClassWithThreeTemplates::class;

        $type = $this->parser->parse("$className<int, string, bool, string>");

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame("Could not find a template to assign the generic(s) `string` for the class `$className`.", $type->message());
    }

    public function test_value_of_enum_missing_opening_bracket_throws_exception(): void
    {
        $type = $this->parser->parse('value-of');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The opening bracket is missing for `value-of<...>`.', $type->message());
    }

    public function test_value_of_enum_missing_subtype_throws_exception(): void
    {
        $type = $this->parser->parse("value-of<");

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('The subtype is missing for `value-of<`.', $type->message());
    }

    public function test_value_of_enum_missing_closing_bracket_throws_exception(): void
    {
        $enumName = BackedStringEnum::class;

        $type = $this->parser->parse("value-of<$enumName");

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame("The closing bracket is missing for `value-of<$enumName>`.", $type->message());
    }

    public function test_value_of_incorrect_type_throws_exception(): void
    {
        $type = $this->parser->parse('value-of<string>');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Invalid subtype `value-of<string>`, it should be a `BackedEnum`.', $type->message());
    }

    public function test_value_of_unit_enum_type_throws_exception(): void
    {
        $enumName = PureEnum::class;

        $type = $this->parser->parse("value-of<$enumName>");

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame("Invalid subtype `value-of<$enumName>`, it should be a `BackedEnum`.", $type->message());
    }

    public function test_nullable_type_missing_right_type_throws_exception(): void
    {
        $type = $this->parser->parse('?');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Missing right type for nullable type after `?`.', $type->message());
    }

    public function test_type_expected_for_callable_throws_exception(): void
    {
        $type = $this->parser->parse('callable(');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Expected type after `callable(`.', $type->message());
    }

    public function test_closing_parenthesis_expected_for_callable_throws_exception(): void
    {
        $type = $this->parser->parse('callable(string, int');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Expected closing parenthesis after `callable(string, int`.', $type->message());
    }

    public function test_colon_expected_after_callable_closing_parenthesis_throws_exception(): void
    {
        $type = $this->parser->parse('callable(string, int)');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Expected `:` to define return type after `callable(string, int)`.', $type->message());
    }

    public function test_unexpected_token_after_callable_closing_parenthesis_throws_exception(): void
    {
        $type = $this->parser->parse('callable(string, int)<');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Expected `:` to define return type after `callable(string, int)`, got `<`.', $type->message());
    }

    public function test_return_type_expected_after_callable_colon_throws_exception(): void
    {
        $type = $this->parser->parse('callable(string, int):');

        self::assertInstanceOf(UnresolvableType::class, $type);
        self::assertSame('Expected return type after `callable(string, int):`.', $type->message());
    }
}

/**
 * @template TemplateA
 */
final class SomeClassWithOneTemplate {}

/**
 * @template TemplateA
 * @template TemplateB
 * @template TemplateC
 */
final class SomeClassWithThreeTemplates {}

/**
 * @template TemplateA of array-key
 */
final class SomeClassWithTemplateOfArrayKey {}

/**
 * @template TemplateA
 * @template TemplateB of object
 */
final class SomeClassWithFirstTemplateWithoutTypeAndSecondTemplateWithType {}

/**
 * @template TemplateA
 */
interface SomeInterfaceWithOneTemplate {}

enum EnumWithNoCase {}
